/*
 * Copyright 2010 Dovado FZ-LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dovado.dovadoremote;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.StringTokenizer;

import android.os.Looper;

public final class Communicator {
	/*
	 * Declarations for standard values:
	 */
	private static String host = "192.168.0.1";
	private static final int port = 6435;
	private static String username = "admin";
	private static String password = "password";

	/*
	 * Waiting for response from sent command (char 23)
	 */
	private static final int command_timeout = 100000;

	/*
	 * Socket timeout. could be lower
	 */
	private static final int connection_timeout = 5000;

	/*
	 * Declarations of I/O handlers:
	 */
	static Socket APISocket = null;
	static DataOutputStream os = null;
	static DataInputStream is = null;
	static java.io.BufferedReader reader = null;

	/*
	 * Just some constants for commands:
	 */
	static final String aliases = "ts aliases";
	static final String info = "info";
	private static final String groups = "ts groups";
	private static boolean looper_prepared = false;
	/*
	 * Constants for different alias types:
	 */
	private static final int TYPE_REGULAR_ALIAS = 0;
	private static final int TYPE_GROUP = 1;
	private static final int TYPE_DIM_ALIAS = 2;

	/*
	 * timing, used for debug only:
	 */
	private static long startTime;

	/*
	 * Will be initiated by main class on startup
	 */
	private static String settingsFile = "";

	/*
	 * Which router is it we're connected to? needed for special types of
	 * messages and handling
	 */
	private static String routerModel = "Not available";

	/*
	 * Stored message which will contain message regarding login-attempts (for
	 * example successful, failed etc)
	 */
	private static String loginMessage = "";

	/*
	 * Boolean which is true if a failed login-attempt should show the
	 * "auto-enable"-dialog (only on connectivity errors, not on bad pw errors)
	 */
	private static boolean autoenable;

	static Homeautomation automationObject;
	static Info infoClass;

	/*
	 * closes the connection. Used by other activities
	 */
	public static void closeConnection() {
		try {
			os.flush();
			os.close();
			is.close();
			APISocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/*
	 * Establish TCP/IP connection to host
	 */
	public static boolean connect() {

		/*
		 * Init the port and input/output streams:
		 */
		try {
			/*
			 * Create socket:
			 */
			APISocket = new Socket();
			APISocket.bind(null);
			APISocket.connect(new InetSocketAddress(host, port),
					connection_timeout);
			try {
				APISocket.setSoTimeout(command_timeout);
			} catch (SocketException e) {
				e.printStackTrace();
			}
			/*
			 * Create input/output streams and a reader:
			 */
			os = new DataOutputStream(APISocket.getOutputStream());
			is = new DataInputStream(APISocket.getInputStream());
			reader = new BufferedReader(new InputStreamReader(is));

		} catch (UnknownHostException e) {
			System.err.println("Don't know about host: " + host.toString());
			return false;
		} catch (IOException e) {
			System.err.println("Couldn't get I/O for the connection to: "
					+ host.toString());
			return false;
		}
		return true;
	}

	public static String getHost() {
		return host;
	}

	/*
	 * Returns the current saved hostname (from textfile if available, else
	 * default 192.168.0.1)
	 */
	public static String getHost(String filename) {
		readFile(filename);
		return host;
	}

	/*
	 * Sends the command and returns whatever the answer is as an object (often
	 * an ArrayList)
	 */
	private static Object getInfo(String command) {
		/*
		 * send the command from the argument (eg "ts aliases"): Read every line
		 * from the answer into an array and return an answer do something with
		 * that answer depending on the command
		 */
		ArrayList<String> allInfo = sendCommand(command);

		int i = 0;
		/*
		 * If the command was "ts aliases" then we want to get all Aliases (for
		 * homeautomation)
		 */
		if (command.equalsIgnoreCase(aliases)) {
			ArrayList<String> tmp_aliases = new ArrayList<String>();
			new ArrayList<String>();
			String alias_temp = "";
			/*
			 * Read every string in the array
			 */
			while (i < allInfo.size()) {
				/*
				 * Look for "ALIAS" (line example ALIAS='bedroomlamp')
				 */
				if (allInfo.get(i).indexOf("ALIAS") != -1) {
					StringTokenizer st = new StringTokenizer(allInfo.get(i),
							"=");
					while (st.hasMoreTokens()) {
						st.nextToken();
						String val = st.nextToken();
						/*
						 * remove first and last character if <'> is found
						 */
						// System.err.println("val stringtok=<" +
						// val.substring(0, 1) + ">");
						if (val.substring(0, 1).equalsIgnoreCase("'")) {
							val = val.substring(1, val.length() - 1);
						}
						/*
						 * Put the ALIAS (eg bedroomlamp) in a temporary var and
						 * put it in the return array as soon as we know which
						 * protocol is associated with it:
						 */
						// System.err.println("adding <" + val + ">");
						alias_temp = val;
					}
				}
				/*
				 * After alias, we will also read protocol (that's how the
				 * config file is assembled)
				 */
				if (allInfo.get(i).indexOf("PROTOCOL") != -1) {
					StringTokenizer st = new StringTokenizer(allInfo.get(i),
							"=");
					while (st.hasMoreTokens()) {
						st.nextToken();
						String val = st.nextToken();
						/*
						 * If we encounter a dimmer then add a warning into the
						 * array that the next element is a dimmer:
						 */
						if (val.contains("DIMMER")) {
							tmp_aliases.add("DIMMER:");
						}
						/*
						 * Now add the alias (name) to the return-array
						 */
						tmp_aliases.add(alias_temp);

					}
				}
				i++;
			}
			return tmp_aliases;
		}
		/*
		 * If we're looking for groups
		 */
		if (command.contains(groups)) {
			ArrayList<String> tmp_groups = new ArrayList<String>();
			while (i < allInfo.size()) {
				if (allInfo.get(i).indexOf("GROUP_ALIAS") != -1) {
					StringTokenizer st = new StringTokenizer(allInfo.get(i),
							"=");
					while (st.hasMoreTokens()) {
						st.nextToken();
						String val = st.nextToken();
						/*
						 * remove first and last character if <'> is found
						 */
						if (val.substring(0, 1).equalsIgnoreCase("'")) {
							val = val.substring(1, val.length() - 1);
						}
						/*
						 * Put the group (eg bedroom) in the return-array:
						 */
						tmp_groups.add(val);
					}
				}
				i++;
			}
			return tmp_groups;
		}
		/*
		 * Info
		 */
		else if (command.contains(info)) {
			/*
			 * If we're looking for info then just return the original array.
			 */
			return allInfo;
		}

		return null;

	}

	public static String getLoginMessage() {
		return loginMessage;
	}

	public static String getPassword() {
		return password;
	}

	/*
	 * Returns the password (looking in config file first and takes 'password'
	 * if none is found).
	 */
	public static String getPassword(String filename) {
		readFile(filename);
		return password;
	}

	public static String getRouterModel() {
		return routerModel;
	}

	/*
	 * get the settings-file
	 */
	public static String getSettingsFile() {
		return settingsFile;
	}

	public static boolean getShowAutoenable() {
		return autoenable;
	}

	public static String getUsername() {
		return username;
	}

	/*
	 * Returns the current saved username (from textfile if available, else
	 * default 192.168.0.1)
	 */
	public static String getUsername(String filename) {
		readFile(filename);
		return username;
	}

	/*
	 * Load all aliases and groups.
	 */

	public static void loadAliasesAndGroups() {
		/*
		 * Save each alias in the array fetched_aliases
		 */
		ArrayList<String> tmp_aliases = (ArrayList<String>) getInfo(aliases);
		ArrayList<String> tmp_groups = (ArrayList<String>) getInfo(groups);

		/*
		 * Instantiate a Homeautomation and add aliases and groups to it (let's
		 * hope the garbage collector handles this properly):
		 */
		// The Looper provides a message queue required by the Handler.
		prepareLooper();

		/*
		 * I'm not sure if this is needed at all times
		 */
		automationObject.removeAll();

		int i = 0;
		/*
		 * Add aliases (with special for dimmers)
		 */
		while (i < tmp_aliases.size()) {
			if (tmp_aliases.get(i).equalsIgnoreCase("DIMMER:")) {
				i++;

				/*
				 * add dimmer as type DIMMER:
				 */
				automationObject.add(tmp_aliases.get(i), TYPE_DIM_ALIAS);

				/*
				 * add dimmers as a regular alias too:
				 */
				automationObject.add(tmp_aliases.get(i), TYPE_REGULAR_ALIAS);
			} else {
				automationObject.add(tmp_aliases.get(i), TYPE_REGULAR_ALIAS);
			}
			i++;
		}
		i = 0;

		/*
		 * Add groups:
		 */
		while (i < tmp_groups.size()) {
			automationObject.add(tmp_groups.get(i), TYPE_GROUP);
			i++;
		}
	}

	/*
	 * Called from outside, loads info into the info-activity
	 */
	@SuppressWarnings("unchecked")
	public static void loadInfo() {
		/*
		 * Create a new Info and add the obtained information to it.
		 */
		ArrayList<String> tmp_info = (ArrayList<String>) getInfo(info);

		infoClass.removeAll();
		infoClass.createInfoList(tmp_info);
		/*
		 * This variable can be used to display different views for different
		 * routers. You might for example want something visible for the 4GR but
		 * not for the UMR
		 */
		setRouterModel(Info.parseInfo("PRODUCT_NAME="));
	}

	/*
	 * Login on the host:
	 */
	public static boolean login() {
		if (APISocket != null && os != null && is != null) {

			System.err.println("Executing login routine..");

			/*
			 * send username & password:
			 */
			String tempResponse;
			try {
				tempResponse = sendCommand("user " + username).toString();
				System.err.println("OUR RESPONSE WAS:" + tempResponse);
			} catch (Exception e) {
				setLoginMessage("Login error");
				System.err.println("Login error!");
				setShowAutoenable(true);
				return false;
			}
			if (tempResponse.contains("Another active API already running")) {
				setLoginMessage("Another user is currently logged on");
				System.err.println("Another user is currently logged on");
				setShowAutoenable(false);
				return false;

			}

			/*
			 * Did we get access?
			 */
			try {
				tempResponse = sendCommand("pass " + password).toString();
				// System.err.println("OUR RESPONSE WAS:" +tempResponse);
			} catch (Exception e) {

				setLoginMessage("Login error");
				System.err.println("Login error!");
				setShowAutoenable(true);
				return false;
			}

			if (tempResponse.contains("granted")) {
				setLoginMessage("Login successful");
				System.err.println("Login successful!");
				setShowAutoenable(false);
				return true;
			} else if (tempResponse.contains("Wrong username")) {
				setLoginMessage("Invalid username and/or password");
				System.err.println("Invalid username and/or password");
				setShowAutoenable(false);
				return false;
			} else {
				setLoginMessage("Login error");
				System.err.println("Login error!");
				setShowAutoenable(true);
				return false;
			}

		} else {
			setLoginMessage("Login error");
			System.err.println("Login error!");
			setShowAutoenable(true);
			return false;
		}
	}

	/*
	 * We don't really need to use this silly function. It was used when we
	 * created new Homeautomation and Info objects upon each refresh but that
	 * wasn't really necessary. TODO: delete this function
	 */
	public static void prepareLooper() {
		if (!looper_prepared) {
			Looper.prepare();
			automationObject = new Homeautomation();
			infoClass = new Info();
			looper_prepared = true;
		}
	}

	/*
	 * Waits for character 23 (which is selected as escape char by Dovado), the
	 * welcome message (which is not 23-terminated), or command_timeout seconds.
	 * Then returns whatever was received in an arraylist of strings
	 */
	private static ArrayList<String> readAnswer() {
		ArrayList<String> answerArray = new ArrayList<String>();
		String line = "";
		try {
			/*
			 * Read from the router:
			 */
			line = reader.readLine();
			while (true) {

				/*
				 * Dreadful piece of code: look for escape char or welcome
				 * message. Timeouts after command_timeout seconds
				 */
				if (line != null) {
					if (line.length() != 0) {
						if (line.indexOf(23) != -1) {
							break;
						}
						if (line.contains("Another active API")) {
							answerArray.add(line);
							break;
						}

						answerArray.add(line);
					}
					// System.err.println("line reads: " + line + "*");
				}
				line = reader.readLine();
			}
		} catch (SocketTimeoutException e) {
			/*
			 * return what we got so far if we encounter a timeout:
			 */
			System.err
					.println("timeout error reading from socket. check char23-terminations!");
			return answerArray;
		} catch (Exception e) {
			System.err.println("error " + line + "errorcode " + e);
			return null;

		}

		/*
		 * return the answer:
		 */
		return answerArray;
	}

	/*
	 * Reads current settings (hostname+password) from file Must be decoded
	 * using ISO-8859-1
	 */
	public static void readFile(String filename) {
		File f = new File(filename);
		FileInputStream fileIS = null;
		try {
			fileIS = new FileInputStream(f);
			BufferedReader buf = new BufferedReader(new InputStreamReader(
					fileIS));
			try {

				// System.err.println("Reading username=" + username);

				// read host
				String tmphost = buf.readLine();
				if (tmphost != null) {
					if (!tmphost.equals("")) {
						host = tmphost;
					}
				}
				// System.err.println("Reading Host=" + host);

				// read password
				// System.err.println("Reading pre decode=" + tmppassword);
				String tmppassword = buf.readLine();
				if (tmppassword != null) {
					tmppassword = URLDecoder.decode(tmppassword, "ISO-8859-1");
					if (!tmppassword.equals("")) {
						password = tmppassword;
					}
				}
				// System.err.println("Reading Password (post decode)=" +
				// password);

				// read username
				String tmpuser = buf.readLine();
				if (tmpuser != null) {
					if (!tmpuser.equals("")) {
						username = tmpuser;
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	/*
	 * Send a command and do nothing else. Now we just throw any exception and
	 * hope that we got -something- before the error
	 */
	public static ArrayList<String> sendCommand(String command) {
		// System.err.println("About to execute: <" + command + ">");
		try {
			os.writeBytes(command + "\r\n");
			/*
			 * Do not wait for answer if we're sending quit-command
			 */
			if (!command.contains("quit")) {
				return readAnswer();
			} else {
				return null;
			}

		} catch (UnknownHostException e) {
			System.err.println("Trying to connect to unknown host: " + e);
			return readAnswer();
		} catch (IOException e) {
			System.err.println("IOException:  " + e);
			return readAnswer();
		}
	}

	/*
	 * Set the current host
	 */
	public static void setHost(String inHost) {
		host = inHost;
	}

	public static void setLoginMessage(String inMessage) {
		loginMessage = inMessage;
	}

	/*
	 * Set the current password encoding it to ISO-8859-1
	 */
	public static void setPassword(String inPassword) {
		try {
			inPassword = URLEncoder.encode(inPassword, "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		// System.err.println("Setting (encoded) password to:<"+
		// inPassword+">");
		password = inPassword;
	}

	public static void setRouterModel(String inRouterModel) {
		routerModel = inRouterModel;
	}

	/*
	 * set the settings-file
	 */
	public static void setSettingsFile(String filename) {
		settingsFile = filename;
	}

	/*
	 * These functions sets and gets whether the GUI should display
	 * "do you want to auto-activate?":
	 */
	public static void setShowAutoenable(boolean inAutoenable) {
		autoenable = inAutoenable;
	}

	/*
	 * Set the current user
	 */
	public static void setUsername(String inUsername) {
		username = inUsername;
	}

	/*
	 * Timers, just for debugging and optimizations
	 */
	public static void startTimer() {
		startTime = System.currentTimeMillis();
	}

	/*
	 * Timers, just for debugging and optimizations
	 */
	public static void stopTimer(String msg) {
		System.err.println("TIME of <" + msg + "> : "
				+ (System.currentTimeMillis() - startTime));
	}

	/*
	 * method which connects, logs in and receives all info+homeautomation
	 * aliases. Thereafter gives the current coordinates to the Map-class and it
	 * disconnects. This function also sets the message to be displayed upon
	 * refresh ("login failed", "success", etc)
	 */
	public static boolean sync() {
		/*
		 * Open connection
		 */
		if (Communicator.connect()) {
			/*
			 * Login
			 */
			try {
				/*
				 * Login Successful:
				 */
				if (Communicator.login()) {
					/*
					 * Sync up! Start by loading aliases and groups
					 */
					Communicator.loadAliasesAndGroups();
					/*
					 * Load various info (signal strength etc)
					 */
					Communicator.loadInfo();
					/*
					 * quit
					 */
					Communicator.sendCommand("quit");
					/*
					 * close the connection
					 */
					Communicator.closeConnection();
					/*
					 * Set coordinates on the map
					 */
					Map.updateCoordinates();

					/*
					 * We have synced
					 */
					DAAPP.setHasSynced(true);
					DAAPP.setIsSynced(true);
					setLoginMessage("Refresh successful");
					return true;
				}
				/*
				 * Login Failed:
				 */
				else {
					System.err.println("Login failed!");
					Communicator.closeConnection();
					DAAPP.setIsSynced(false);
					return false;
				}
			} catch (RuntimeException e) {
				/*
				 * We never arrive here anymore (this was a debug-related catch
				 * when re-syncing and re-creating Homeautomation and Info).
				 */
				Communicator.closeConnection();
				DAAPP.setIsSynced(true);
				setLoginMessage("Refresh successful");
				return true;

			} catch (Exception e) {
				System.err.println("TOTAL SYNC ERROR!!" + e);
				Communicator.closeConnection();
				DAAPP.setIsSynced(false);
				setLoginMessage("Refresh failed");
				setShowAutoenable(true);
				return false;

			}
		}
		/*
		 * If no connectivity:
		 */
		else {
			setLoginMessage("Login error");
			System.err.println("Login error!");
			setShowAutoenable(true);
			DAAPP.setIsSynced(false);
			return false;
		}

	}

}
